package trading_strategy.strategies;

import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartTheme;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.CandlestickRenderer;
import org.jfree.chart.renderer.xy.XYSplineRenderer;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.ohlc.OHLCSeries;
import org.jfree.data.time.ohlc.OHLCSeriesCollection;
import trading_strategy.Strategy;
import trading_strategy.events.AbstractMarketEvent;
import trading_strategy.events.EventManager;
import trading_strategy.events.filtered.AMMFilter;
import trading_strategy.events.filtered.Candle;
import trading_strategy.events.filtered.CandleFilter;
import trading_strategy.instruments.ListedInstrument;
import trading_strategy.orders.Order;
import trading_strategy.orders.SingleOrderAutomaton;
import trading_strategy.positions.InstrumentPosition;
import trading_strategy.positions.Trade;

import javax.swing.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.TreeMap;
import java.util.concurrent.Semaphore;

/**
 * Created by IntelliJ IDEA.
 * User: gderoujoux
 * Date: 24 mai 2010
 * Time: 16:26:16
 * To change this template use File | Settings | File Templates.
 */
public class MMAStrategy extends Strategy implements SingleOrderAutomaton.OrderProvider, TriggerPositionEnterer.PositionEntererListener {
    public static boolean INTERACTIVE = false;
    private AMMFilter amm28;
    private int candleDurationInSeconds = 120;

    class CrossedMMa extends AbstractMarketEvent {
        Direction direction;
        double entryLevel;
        double exitLevel;
        long maxTsValidity;

        CrossedMMa(long ts, Direction direction, double entryLevel, double exitLevel, long maxTsValidity) {
            super(ts);
            this.direction = direction;
            this.entryLevel = entryLevel;
            this.exitLevel = exitLevel;
            this.maxTsValidity = maxTsValidity;
        }
    }


    public static final Logger LOGGER = Logger.getLogger(MMAStrategy.class);
    SingleOrderAutomaton automaton;
    private TriggerPositionExiter positionExiter;
    private TriggerPositionEnterer positionEnterer;
    private int eventCounter = 0;
    private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
    private Date date = this.date;

    private InstrumentPosition position;
    private static final char CSV_SEPARATOR = ';';

    enum State {
        PASSIVE, SIGNAL_DETECTED, IN_POSITION
    }

    ListedInstrument instrument;
    TreeMap<Long, Candle> lastCandles = new TreeMap<Long, Candle>();
    TreeMap<Long, AMMFilter.AMMEvent> mmaEvents = new TreeMap<Long, AMMFilter.AMMEvent>();

    @Override
    public double getPnL() {
        return position.getPnL();
    }

    public double getTransactionCost() {
        return position.getTransactioCost();
    }

    AMMFilter.AMMEvent lastAMMEvent;
    State state = State.PASSIVE;


    public MMAStrategy(Date date, ListedInstrument instrument) {
        this.date = date;
        this.instrument = instrument;
        this.automaton = new SingleOrderAutomaton(instrument, this);
        this.position = new InstrumentPosition(instrument);
    }

    @Override
    public void onMarketEvent(AbstractMarketEvent event) {
        boolean shouldCheckCandleSignal = false;
        if (event instanceof AMMFilter.AMMEvent) {
            AMMFilter.AMMEvent ammEvent = (AMMFilter.AMMEvent) event;
            lastAMMEvent = ammEvent;
            mmaEvents.put(ammEvent.getTs(), ammEvent);
            shouldCheckCandleSignal = true;
        } else if (event instanceof CandleFilter.CandleEvent) {
            eventCounter++;
            CandleFilter.CandleEvent ce = (CandleFilter.CandleEvent) event;
            Candle candle = ce.getCandle();
            lastCandles.put(event.getTs(), candle);
            shouldCheckCandleSignal = true;
        }

        if (shouldCheckCandleSignal) {
            detectMMACrossing(event);
        }
        if (event instanceof CrossedMMa) {
            CrossedMMa crossedEvent = (CrossedMMa) event;
            LOGGER.info("Signal " + crossedEvent.direction + "@" + crossedEvent.entryLevel + " at " + crossedEvent.getDate());
            if (state == State.IN_POSITION && positionExiter != null && positionExiter.direction != crossedEvent.direction) {
                positionExiter.hit(event.getTs(), "Opposite signal");
            } else if (state == State.SIGNAL_DETECTED) {
                if (positionEnterer != null) {
                    positionEnterer.deactivated(event.getTs());
                }
                positionEnterer = new TriggerPositionEnterer(this, instrument, crossedEvent.direction, crossedEvent.entryLevel, crossedEvent.exitLevel, crossedEvent.maxTsValidity * 1000l, automaton);
            } else if (state == State.PASSIVE) {
                state = State.SIGNAL_DETECTED;
                positionEnterer = new TriggerPositionEnterer(this, instrument, crossedEvent.direction, crossedEvent.entryLevel, crossedEvent.exitLevel, crossedEvent.maxTsValidity * 1000l, automaton);
            }
        }
    }

    private void detectMMACrossing(AbstractMarketEvent currentEvent) {
        if (lastAMMEvent == null || currentEvent.getDate().getTime() - lastAMMEvent.getDate().getTime() > 20000l) {
            // AMM is too old to be taken seriously, ignore and wait for next AMM
            return;
        }

        // We need enough candles
        int nbObs = 10;
        if (lastCandles.size() < nbObs || mmaEvents.size() < nbObs) {
            return;
        }

        try {
            Date d = DateUtils.parseDate("20100107-15:50:00", new String[]{"yyyyMMdd-HH:mm:ss"});
            if (currentEvent.getDate().after(d)) {
                int i = 0;
            }
        } catch (Exception e) {
        }
        double[] distToMma = new double[nbObs];
        ArrayList<Long> keys = new ArrayList<Long>(lastCandles.keySet());
        Candle[] candles = new Candle[nbObs];
        double minLow = Double.MAX_VALUE;
        double maxHigh = 0;
        for (int i = 0; i < distToMma.length; i++) {
            Long ts = keys.get(keys.size() - i - 1);
            if (!mmaEvents.containsKey(ts)) {
                return;
            }
            Candle candle = lastCandles.get(ts);
            candles[i] = candle;
            AMMFilter.AMMEvent event = mmaEvents.get(ts);
            if (i < distToMma.length - 1) {
                minLow = Math.min(minLow, candle.getLow());
                maxHigh = Math.max(maxHigh, candle.getHigh());
            }
            distToMma[distToMma.length - i - 1] = event.getValue() - candle.getVwap();
        }

        /*
        // Check that the first 3 items have the same sign
        for (int i = 1; i < distToMma.length - 1; i++) {
            if (distToMma[i] * distToMma[i - 1] < 0) {
                return;
            }
        }
        */
        double minDist = Double.MAX_VALUE;
        double maxDist = Double.MIN_VALUE;

        for (int i = 0; i < distToMma.length; i++) {
            minDist = Math.min(minDist, distToMma[i]);
            maxDist= Math.max(maxDist, distToMma[i]);
        }

        double lastDist = distToMma[distToMma.length - 1];
        /*
        if (lastDist * distToMma[distToMma.length - 2] > 0) {
            return;
        }
        */

        if (Math.abs(lastDist) > 2) {
            Candle lastCandle = lastCandles.lastEntry().getValue();
            AMMFilter.AMMEvent lastMma = mmaEvents.lastEntry().getValue();
            Strategy.Direction dir = null;
            double entryLevel = 0;
            double exitLevel = 0;
            if (lastDist < 0 && minDist < -1 && maxDist > 3) {
                dir = Direction.BUY;
                entryLevel = lastCandle.getHigh() + 1;
                exitLevel = lastMma.getValue() - 1;
            } else if (lastDist > 0 && maxDist > 1 && minDist < -3) {
                dir = Direction.SELL;
                entryLevel = lastCandle.getLow() - 1;
                exitLevel = lastMma.getValue() + 1;
            }
            if (dir != null) {
                getEventDispatcher().addScheduledEvent(new CrossedMMa(currentEvent.getTs(), dir, entryLevel, exitLevel, currentEvent.getDate().getTime() + 300 * 1000l));
            }
        }
    }

    @Override
    public boolean shouldStillSendOrder(Order order) {
        return true;
    }

    @Override
    public void onExecution(int qty, double price, Order order) {
        Trade trade = new Trade(order.getMatchingTimestamp(), date, qty, price, getClass().getName());
        position.addTrade(trade);
        if (state == State.SIGNAL_DETECTED) {
            if (positionEnterer != null) {
                positionEnterer.deactivated(order.getMatchingTimestamp());
                LOGGER.info("Position entered " + qty + "@" + price + " at " + new Date(order.getMatchingTimestamp() / 1000));
                positionEnterer = null;
                long mustExitBefore = order.getMatchingTimestamp() + 10 * candleDurationInSeconds * 1000000l;
                long mustConfirmBefore = order.getMatchingTimestamp() + candleDurationInSeconds * 1000000l;
                positionExiter = new TriggerPositionExiter(instrument, this, automaton, trade, 3, 10, 1, mustExitBefore, mustConfirmBefore, amm28);
                state = State.IN_POSITION;
            }
        } else if (state == State.IN_POSITION) {
            if (positionExiter != null) {
                positionExiter.deactivated(order.getMatchingTimestamp());
                LOGGER.info("Position exited " + qty + "@" + price + " at " + new Date(order.getMatchingTimestamp() / 1000));
                positionExiter = null;
                state = State.PASSIVE;
            }
        }
        dumpGraph();
    }

    public void register() {
        // Subscribe to events classes
        CandleFilter cf = new CandleFilter(instrument, "candles", candleDurationInSeconds);
        cf.subscribe();
        amm28 = new AMMFilter(instrument, "AMM28", 20, cf);
        amm28.subscribe();

        // Make sure that we subscribe to AMM before candle, in order to get the AMM before the candle...
        EventManager.getInstance().subscribe(getListener(), amm28);
        EventManager.getInstance().subscribe(getListener(), cf);
    }

    public int getEventCounter() {
        return eventCounter;
    }

    @Override
    public void deactivated() {
        if (state == State.SIGNAL_DETECTED) {
            state = State.PASSIVE;
        }
    }

    @Override
    public void stop() {
        super.stop();
    }

    public void dumpGraph() {
        dumpGraph(INTERACTIVE);
    }

    public void dumpGraph(boolean interactive) {
        if (!interactive) {
            return;
        }
        OHLCSeriesCollection candleDataset = new OHLCSeriesCollection();
        OHLCSeries series = new OHLCSeries(instrument.getId());
        for (final Candle candle : lastCandles.values()) {
            RegularTimePeriod period = new RegularTimePeriod() {
                @Override
                public RegularTimePeriod previous() {
                    throw new RuntimeException("Not implemented");
                }

                @Override
                public RegularTimePeriod next() {
                    throw new RuntimeException("Not implemented");
                }

                @Override
                public long getSerialIndex() {
                    return 0;  //To change body of implemented methods use File | Settings | File Templates.
                }

                @Override
                public void peg(Calendar calendar) {
                    throw new RuntimeException("Not implemented");
                }

                @Override
                public long getFirstMillisecond() {
                    return candle.getStart().getTime();
                }

                @Override
                public long getFirstMillisecond(Calendar calendar) {
                    throw new RuntimeException("Not implemented");
                }

                @Override
                public long getLastMillisecond() {
                    return candle.getEnd().getTime();
                }

                @Override
                public long getLastMillisecond(Calendar calendar) {
                    throw new RuntimeException("Not implemented");
                }

                @Override
                public int compareTo(Object o) {
                    RegularTimePeriod rtp = (RegularTimePeriod) o;
                    return (int) (getStart().getTime() - rtp.getStart().getTime());
                }
            };

            series.add(period, candle.getOpen(), candle.getHigh(), candle.getLow(), candle.getClose());
        }
        candleDataset.addSeries(series);

        NumberAxis priceAxis = new NumberAxis("Price");
        DateAxis timeAxis = new DateAxis("Time");

        XYPlot plot = new XYPlot(); //candleDataset, timeAxis, priceAxis, new CandlestickRenderer());
        timeAxis.setAutoRange(true);
        priceAxis.setAutoRangeIncludesZero(false);
        priceAxis.setAutoRange(true);
        plot.setDataset(0, candleDataset);
        plot.setRenderer(0, new CandlestickRenderer());
        plot.setDomainAxis(0, timeAxis);
        plot.setRangeAxis(0, priceAxis);
        plot.mapDatasetToDomainAxis(0, 0);
        plot.mapDatasetToRangeAxis(0, 0);

        TimeSeries mmaSeries = new TimeSeries("MMA");
        for (AMMFilter.AMMEvent ammEvent : mmaEvents.values()) {
            try {
                mmaSeries.add(new Millisecond(ammEvent.getDate()), ammEvent.getValue());
            } catch (Exception e) {
                //e.printStackTrace();
            }
        }
        TimeSeriesCollection timeSeries = new TimeSeriesCollection();
        timeSeries.addSeries(mmaSeries);
        plot.setDataset(1, timeSeries);
        plot.setRenderer(1, new XYSplineRenderer());
        plot.setDomainAxis(1, timeAxis);
        plot.setRangeAxis(1, priceAxis);
        plot.mapDatasetToDomainAxis(1, 0);
        plot.mapDatasetToRangeAxis(1, 0);

        ChartTheme currentTheme = new StandardChartTheme("JFree");
        final JFreeChart chart = new JFreeChart("Candle-" + date, JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        currentTheme.apply(chart);

        final Semaphore wait = new Semaphore(0);
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                ChartPanel chartPanel = new ChartPanel(chart, true, true, true, true, true);
                JFrame jFrame = new JFrame("Chart");
                jFrame.add(chartPanel);
                jFrame.pack();
                jFrame.addWindowListener(new WindowListener() {
                    @Override
                    public void windowOpened(WindowEvent e) {
                        //To change body of implemented methods use File | Settings | File Templates.
                    }

                    @Override
                    public void windowClosing(WindowEvent e) {
                        wait.release();
                    }

                    @Override
                    public void windowClosed(WindowEvent e) {
                        wait.release();
                    }

                    @Override
                    public void windowIconified(WindowEvent e) {
                        //To change body of implemented methods use File | Settings | File Templates.
                    }

                    @Override
                    public void windowDeiconified(WindowEvent e) {
                        //To change body of implemented methods use File | Settings | File Templates.
                    }

                    @Override
                    public void windowActivated(WindowEvent e) {
                        //To change body of implemented methods use File | Settings | File Templates.
                    }

                    @Override
                    public void windowDeactivated(WindowEvent e) {
                        //To change body of implemented methods use File | Settings | File Templates.
                    }
                });
                jFrame.setVisible(true);
            }
        });
        try {
            wait.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }
}